import Foundation
import GRDB
import GRDBSQLite
import StructuredQueriesCore

/// A cursor of a structured query.
///
/// Iterates over and decodes all of the rows of a structured query.
public class QueryCursor<Element>: DatabaseCursor {
  public var _isDone = false
  public let _statement: GRDB.Statement

  @usableFromInline
  var decoder: SQLiteQueryDecoder

  @usableFromInline
  init(db: Database, query: QueryFragment) throws {
    (_statement, decoder) = try db.prepare(query: query)
  }

  deinit {
    sqlite3_reset(_statement.sqliteStatement)
  }

  public func _element(sqliteStatement _: SQLiteStatement) throws -> Element {
    fatalError("Abstract method should be overridden in subclass")
  }

  @usableFromInline
  struct DecodingError: Error, CustomStringConvertible {
    let columnIndex: Int
    let columnName: String
    let sql: String

    @usableFromInline
    init(columnIndex: Int, columnName: String, sql: String) {
      self.columnIndex = columnIndex
      self.columnName = columnName
      self.sql = sql
    }

    @usableFromInline
    var description: String {
      """
      Expected column \(columnIndex) (\(columnName.debugDescription)) to not be NULL: …

      \(sql)
      """
    }
  }
}

@usableFromInline
final class QueryValueCursor<QueryValue: QueryRepresentable>: QueryCursor<QueryValue.QueryOutput> {
  public typealias Element = QueryValue.QueryOutput

  // NB: Required to workaround a "Legacy previews execution" bug
  //     https://github.com/pointfreeco/sharing-grdb/pull/60
  @usableFromInline
  override init(db: Database, query: QueryFragment) throws {
    try super.init(db: db, query: query)
  }

  @inlinable
  public override func _element(sqliteStatement _: SQLiteStatement) throws -> Element {
    do {
      let element = try QueryValue(decoder: &decoder).queryOutput
      decoder.next()
      return element
    } catch QueryDecodingError.missingRequiredColumn {
      let columnIndex = Int(decoder.currentIndex) - 1
      throw DecodingError(
        columnIndex: columnIndex,
        columnName: _statement.columnNames[columnIndex],
        sql: _statement.sql
      )
    }
  }
}

@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
@usableFromInline
final class QueryPackCursor<
  each QueryValue: QueryRepresentable
>: QueryCursor<(repeat (each QueryValue).QueryOutput)> {
  public typealias Element = (repeat (each QueryValue).QueryOutput)

  // NB: Required to workaround a "Legacy previews execution" bug
  //     https://github.com/pointfreeco/sharing-grdb/pull/60
  @usableFromInline
  override init(db: Database, query: QueryFragment) throws {
    try super.init(db: db, query: query)
  }

  @inlinable
  public override func _element(sqliteStatement _: SQLiteStatement) throws -> Element {
    do {
      let element = try decoder.decodeColumns((repeat each QueryValue).self)
      decoder.next()
      return element
    } catch QueryDecodingError.missingRequiredColumn {
      let columnIndex = Int(decoder.currentIndex) - 1
      throw DecodingError(
        columnIndex: columnIndex,
        columnName: _statement.columnNames[columnIndex],
        sql: _statement.sql
      )
    }
  }
}

@usableFromInline
final class QueryVoidCursor: QueryCursor<Void> {
  typealias Element = ()

  // NB: Required to workaround a "Legacy previews execution" bug
  //     https://github.com/pointfreeco/sharing-grdb/pull/60
  @usableFromInline
  override init(db: Database, query: QueryFragment) throws {
    try super.init(db: db, query: query)
  }

  @inlinable
  override func _element(sqliteStatement _: SQLiteStatement) throws {
    try decoder.decodeColumns(Void.self)
    decoder.next()
  }
}

extension Database {
  @inlinable
  func prepare(query: QueryFragment) throws -> (GRDB.Statement, SQLiteQueryDecoder) {
    var (sql, bindings) = query.prepare { _ in "?" }
    if sql.isEmpty {
      sql = "SELECT 1 WHERE 0 -- Empty query generated by StructuredQueries"
    }
    let statement = try makeStatement(sql: sql)
    statement.arguments = try StatementArguments(bindings.map { try $0.databaseValue })
    return (
      statement,
      SQLiteQueryDecoder(statement: statement.sqliteStatement)
    )
  }
}

extension QueryBinding {
  @inlinable
  var databaseValue: DatabaseValue {
    get throws {
      switch self {
      case let .blob(blob):
        return Data(blob).databaseValue
      case let .bool(bool):
        return (bool ? 1 : 0).databaseValue
      case let .date(date):
        return date.iso8601String.databaseValue
      case let .double(double):
        return double.databaseValue
      case let .int(int):
        return int.databaseValue
      case .null:
        return .null
      case let .text(text):
        return text.databaseValue
      case let .uint(uint) where uint <= UInt64(Int64.max):
        return uint.databaseValue
      case let .uint(uint):
        throw Int64OverflowError(unsignedInteger: uint)
      case let .uuid(uuid):
        return uuid.uuidString.lowercased().databaseValue
      case let .invalid(error):
        throw error
      }
    }
  }
}

@usableFromInline
struct Int64OverflowError: Error {
  let unsignedInteger: UInt64
  @usableFromInline
  init(unsignedInteger: UInt64) {
    self.unsignedInteger = unsignedInteger
  }
}
